/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package it.tukano.misc;

import it.tukano.swingdeco.containers.JPanelExt;
import it.tukano.swingdeco.controls.JPopupMenuExt;
import it.tukano.swingdeco.text.JTextPaneExt;
import it.tukano.swingdeco.text.SimpleAttributeSetExt;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.text.Element;

/**
 * A text document that grabs and displays the system err-out streams
 * @author utente3
 */
public class ConsoleWrapper {
    private static final PrintStream OUT = System.out;
    private static final PrintStream ERR = System.err;
    
    private final SimpleAttributeSetExt OUTPUT_STYLE = new SimpleAttributeSetExt()
            .setFont(new Font("Monospaced", Font.PLAIN, 12))
            .setForeground(Color.BLACK);
    private final SimpleAttributeSetExt ERROR_STYLE = new SimpleAttributeSetExt()
            .setFont(new Font("Monospaced", Font.PLAIN, 12))
            .setForeground(Color.RED);
    private final JTextPaneExt display = new JTextPaneExt();
    private final JPanelExt panel = new JPanelExt(new BorderLayout())
            .append(display.getScroller());
    private final JPopupMenuExt displayPopup = JPopupMenuExt.newDefaultTextPopup(display);

    /**
     * Instance initializer.
     */
    public ConsoleWrapper() {
        KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, true);
        panel.getInputMap().put(ks, "send_line");
        Action action = new AbstractAction("send_line") {
            /* default serial version uid*/
            private static final long serialVersionUID = 0;
            
            public void actionPerformed(ActionEvent e) {
                ConsoleWrapper.this.createAndNotifyTypedLine();
            }
        };
        panel.getActionMap().put("send_line", action);
    }
    
    /** Returns the default display popup menu that manages cut/copy/paste/clear actions
     on the console output text panel */
    public JPopupMenuExt getDisplayPopup() {
        return displayPopup;
    }
    
    /** Returns the swing component that wraps the text input/output of the console */
    public Component getPanel() {
        return panel;
    }
    
    /** Starts the background thread (demon) that grabs the system error/output */
    public void start() {
        startGrabbingError();
        startGrabbingOutput();
    }
    
    /** Restores the system input/output streams. */
    public void stop() {
        System.setErr(ERR);
        System.setOut(OUT);
    }
    
    private void startGrabbingError() {
        Thread runner = new Thread("Console Wrapper Error Thread") {

            @Override
            public void run() {
                try {
                    ConsoleWrapper.this.doGrabError();
                } catch(Exception ex) {
                    System.setErr(ERR);
                }
            }
        };
        runner.setDaemon(true);
        runner.start();
    }
    
    private void startGrabbingOutput() {
        Thread runner = new Thread() {

            @Override
            public void run() {
                try {
                    ConsoleWrapper.this.doGrabOutput();
                } catch(Exception ex) {
                    System.setOut(OUT);
                }
            }
        };
        runner.setDaemon(true);
        runner.start();
    }

    private void doGrabOutput() throws Exception {
        final Charset charset = Charset.defaultCharset();
        WritableByteChannel ch = new WritableByteChannel() {

            public int write(ByteBuffer src) throws IOException {
                int count = src.remaining();
                appendOutput(charset.decode(src).toString());
                return count - src.remaining();
            }

            public boolean isOpen() {
                return true;
            }

            public void close() throws IOException {
            }
        };
        OutputStream out = Channels.newOutputStream(ch);
        System.setOut(new PrintStream(out));
    }

    private void appendOutput(final String token) {
        OUT.print(token);
        EventQueue.invokeLater(new Runnable() {

            public void run() {
                display.append(token, OUTPUT_STYLE);
            }
        });
    }

    private void doGrabError() throws Exception {
        final Charset charset = Charset.defaultCharset();
        WritableByteChannel ch = new WritableByteChannel() {

            public int write(ByteBuffer src) throws IOException {
                int count = src.remaining();
                appendError(charset.decode(src).toString());
                return count - src.remaining();
            }

            public boolean isOpen() {
                return true;
            }

            public void close() throws IOException {
            }
        };
        OutputStream out = Channels.newOutputStream(ch);
        System.setErr(new PrintStream(out));
    }
    
    private void appendError(final String token) {
        ERR.print(token);
        EventQueue.invokeLater(new Runnable() {

            public void run() {
                display.append(token, ERROR_STYLE);
            }
        });
    }

/* Input listeners */
    
    private final List<ConsoleInputListener> listeners = new CopyOnWriteArrayList<ConsoleInputListener>();
    
    /** Add a listener that will be notified when the user types a line of text and presses enter */
    public void addConsoleInputListener(ConsoleInputListener listener) {
        listeners.add(listener);
    }
    
    /** Removes one of the listeners added with the addConsoleInputListener method */
    public void removeConsoleInputListener(ConsoleInputListener listener) {
        listeners.remove(listener);
    }
    
    /** notifies the listeners that a line of text has been typed */
    private void notifyLineTyped(String line) {
        for (ConsoleInputListener listener : listeners) {
            listener.consoleLineTyped(line);
        }
    }

    /** get the line of text of the previous paragraph and send it to the listeners */
    private void createAndNotifyTypedLine() {
        try {
            int caretPosition = display.getCaretPosition();
            Element defaultRootElement = display.getDocument().getDefaultRootElement();
            int elementIndex = defaultRootElement.getElementIndex(caretPosition);
            if(elementIndex > 0) {//event is produced after enter key press, so the caret will be on the paragraph that follows the input
                Element element = defaultRootElement.getElement(elementIndex - 1);
                int startOffset = element.getStartOffset();
                int endOffset = element.getEndOffset();
                String text = display.getDocument().getText(startOffset, endOffset-startOffset);
                if(!text.isEmpty()) {
                    notifyLineTyped(text);
                }
            }
        } catch(Exception ex) {
            OUT.print("no current line");
        }
    }

    public JComponent getJPanel() {
        return panel;
    }
    
/* Nested types */
    
    /** Type of the listeners that will be notified when the user types a line of text
     in the console panel. */
    public static interface ConsoleInputListener {
        
        /** 
         * The user has typed a line of text in the console and pressed enter.
         * @param text the line that has been typed
         */
        void consoleLineTyped(String text);
    }
}
